Skip to content

feat(enterprise): add data drains for continuous export to S3 / webhook#4440

Merged
waleedlatif1 merged 5 commits intostagingfrom
waleedlatif1/data-drains
May 6, 2026
Merged

feat(enterprise): add data drains for continuous export to S3 / webhook#4440
waleedlatif1 merged 5 commits intostagingfrom
waleedlatif1/data-drains

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Continuously exports workflow logs, job logs, audit logs, copilot chats, and copilot runs to customer-owned S3 buckets or HTTPS webhooks on hourly or daily schedules
  • Pairs with data retention so customers can drain into long-term storage before Sim deletes
  • Built on two registries (DrainSource + DrainDestination) so future destinations are a single-file change
  • At-least-once delivery via opaque cursor that advances only on full success; consumers dedupe on stable row ids
  • SSRF-validated webhooks with DNS pinning, HMAC-SHA256 timestamp signatures, S3 server-side encryption, audit logging on every config and run change
  • Self-hosted gating via DATA_DRAINS_ENABLED / NEXT_PUBLIC_DATA_DRAINS_ENABLED, mirroring data retention

Type of Change

  • New feature

Testing

  • 26 unit tests passing (service, dispatcher, sources, S3, webhook)
  • bun run check:api-validation passing
  • Manually tested S3 + webhook end-to-end including failure paths and cursor replay

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 6, 2026 0:37am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 5, 2026

PR Summary

High Risk
Adds a new scheduled data-export pipeline (new DB tables, background jobs, and external network delivery to S3/webhooks), which is security- and data-handling sensitive. Risk centers on credential handling, SSRF protections, and correctness of cursor/dispatch semantics under failure/retry.

Overview
Adds Enterprise Data Drains: organization owners/admins can create named drains that continuously export selected sources (workflow/job logs, audit logs, copilot chats/runs) as NDJSON to either S3 or an HTTPS webhook, with hourly/daily cadence and at-least-once semantics via an opaque cursor that only advances on full success.

Introduces new persistence and orchestration: data_drains + data_drain_runs tables/migration and serializers/contracts; CRUD/run/test/list-runs API endpoints with audit events; a cron dispatcher (/api/cron/run-data-drains) that claims due drains and enqueues run-data-drain Trigger.dev jobs (including orphaned-run reaping), plus a runner service that chunks, delivers, records locators, and updates run status/cursors.

Adds delivery implementations with security controls: S3 writes with deterministic keys + AES256 SSE and endpoint SSRF checks; webhook delivery with DNS validation + pinned IP, HMAC timestamp signatures, idempotency key, and retry/backoff behavior. Exposes a new Settings UI section (self-hosted gated via DATA_DRAINS_ENABLED / NEXT_PUBLIC_DATA_DRAINS_ENABLED) and adds Enterprise docs for configuration and semantics.

Reviewed by Cursor Bugbot for commit abdb0b6. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR introduces a full data-drains subsystem for continuous export of workflow logs, job logs, audit logs, copilot chats, and copilot runs to customer-owned S3 buckets or HTTPS webhooks on hourly or daily schedules. It pairs with data retention so customers can drain into long-term storage before Sim deletes records.

  • Core export pipeline: Source/destination registries, an at-least-once cursor that advances only on full success, SSRF-validated webhook delivery with HMAC-SHA256 signatures and DNS pinning, and S3 delivery with AES256 SSE and runStartedAt-partitioned keys.
  • Dispatch & reliability: Hourly cron with conditional atomic claim per drain, orphan reaper for worker-crashed runs, per-org enterprise-plan gating with billing-error isolation, and a concurrencyKey to serialize manual + cron runs on the job queue.
  • Security & access: Feature-flag and enterprise-plan gates apply to reads and writes alike; credentials encrypted at rest via AES-256-GCM; audit logged on every config and run event; SSRF protection at both the schema (IP-literal) and DNS-resolution (hostname rebinding) layers.

Confidence Score: 5/5

Safe to merge; all blocking issues from prior review rounds have been addressed and the remaining observations are non-critical.

The implementation is thorough: at-least-once delivery with correct cursor semantics, abort signal propagated end-to-end through HTTP sockets, SSRF validation at both schema and DNS layers, atomic conditional claim preventing duplicate dispatch, orphan reaper for worker crashes, and enterprise-plan + feature-flag gates on all read and write paths.

apps/sim/lib/data-drains/sources/audit-logs.ts (null-workspace query branch has no JSON index, may slow as audit volume grows) and apps/sim/lib/data-drains/dispatcher.ts (enqueue failures leave candidates count inconsistent with dispatched+skipped).

Important Files Changed

Filename Overview
apps/sim/lib/data-drains/service.ts Core drain orchestrator: at-least-once delivery via cursor, cancellation check after for-await, proper session cleanup in finally, and failure-path cursor reset to cursorBefore all look correct.
apps/sim/lib/data-drains/dispatcher.ts Conditional claim + orphan reaper are well-designed; enqueue failures are silently dropped from the dispatched/skipped stats, making the cron summary misleading when the queue is unreliable.
apps/sim/lib/data-drains/destinations/webhook.ts SSRF-validated with DNS pinning, HMAC-SHA256 Stripe-style signatures, per-attempt abort signal threading, and retry/backoff with Retry-After support all look solid after prior round of fixes.
apps/sim/lib/data-drains/destinations/s3.ts SSRF-validated endpoint via validateExternalUrl at schema level plus validateUrlWithDNS at runtime; runStartedAt-partitioned keys; AES256 SSE; lazy endpoint check amortized across chunks.
apps/sim/lib/data-drains/access.ts Feature-flag and enterprise-plan gates now apply to reads and writes alike; owner/admin role enforcement is consistent across all routes.
apps/sim/lib/data-drains/sources/audit-logs.ts Correctly scopes to org workspaces plus null-workspace org-level rows, but the JSON metadata filter for the null-workspace branch has no dedicated index and may cause slow scans as audit volume grows.
apps/sim/lib/data-drains/sources/cursor.ts Millisecond-bucketed composite cursor correctly aligns ORDER BY and predicate expressions; row-value comparison syntax is well-supported by PostgreSQL B-tree indexes.
apps/sim/lib/core/security/input-validation.server.ts Abort listener cleanup via settle wrappers prevents reference accumulation on long-lived signals; signal now threaded through to the underlying http.ClientRequest correctly.
packages/db/migrations/0204_powerful_medusa.sql Correct ON DELETE CASCADE from data_drain_runs to data_drains to organization; composite indexes for due-predicate and cursor-range queries look well-chosen.
apps/sim/lib/data-drains/serializers.ts JSONB destinationConfig is re-validated via destination configSchema before serialization; full row parse through dataDrainSchema ensures unexpected shapes surface as errors.

Sequence Diagram

sequenceDiagram
    participant Cron as Cron (hourly)
    participant Dispatcher as dispatchDueDrains
    participant DB as PostgreSQL
    participant Queue as Job Queue
    participant Runner as run-data-drain task
    participant Source as DrainSource
    participant Dest as DrainDestination

    Cron->>Dispatcher: tick
    Dispatcher->>DB: reapOrphanedRuns
    Dispatcher->>DB: SELECT due drains
    loop per candidate
        Dispatcher->>DB: Enterprise plan check
        Dispatcher->>DB: Conditional UPDATE lastRunAt=now
        Dispatcher->>Queue: enqueue run-data-drain
    end
    Queue->>Runner: execute
    Runner->>DB: INSERT dataDrainRuns running
    Runner->>Source: pages(cursor, signal)
    loop per chunk
        Source->>DB: SELECT rows WHERE cursor
        Source-->>Runner: chunk[]
        Runner->>Dest: deliver(body, signal)
        Dest-->>Runner: locator
    end
    alt signal.aborted
        Runner->>DB: UPDATE status=failed
    else success
        Runner->>DB: TRANSACTION cursor + status=success
    end
    Runner->>Dest: session.close()
Loading

Reviews (20): Last reviewed commit: "test(data-drains): drift guard ensures e..." | Re-trigger Greptile

Comment thread apps/sim/lib/data-drains/destinations/webhook.ts Outdated
Comment thread apps/sim/lib/data-drains/destinations/webhook.ts Outdated
Comment thread apps/sim/lib/data-drains/serializers.ts
Comment thread apps/sim/lib/data-drains/dispatcher.ts
Comment thread apps/sim/lib/data-drains/sources/copilot-chats.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/data-drains/destinations/webhook.ts
Comment thread apps/sim/ee/data-drains/hooks/data-drains.ts
Comment thread apps/sim/lib/data-drains/destinations/webhook.ts
Comment thread apps/sim/lib/data-drains/destinations/s3.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/api/contracts/data-drains.ts Outdated
Comment thread apps/sim/lib/core/config/feature-flags.ts
Comment thread apps/sim/lib/data-drains/destinations/s3.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/data-drains/dispatcher.ts
Comment thread apps/sim/lib/data-drains/dispatcher.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/core/security/input-validation.server.ts
Comment thread apps/sim/lib/data-drains/destinations/s3.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/ee/data-drains/destinations/registry.tsx
Comment thread apps/sim/lib/data-drains/destinations/webhook.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/ee/data-drains/hooks/data-drains.ts Outdated
Comment thread apps/sim/ee/data-drains/components/data-drains-settings.tsx
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 62c8b53. Configure here.

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/ee/data-drains/destinations/registry.tsx Outdated
Comment thread apps/sim/lib/data-drains/destinations/webhook.ts
waleedlatif1 and others added 2 commits May 5, 2026 17:32
…orced

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…sim-signature

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Asserts that any header buildHeaders writes is rejected when reused as a
custom signatureHeader. Adding a new metadata header without mirroring it
into RESERVED_SIGNATURE_HEADER_NAMES now fails CI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1 waleedlatif1 merged commit d721dc3 into staging May 6, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/data-drains branch May 6, 2026 01:04
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit abdb0b6. Configure here.

await db
.update(dataDrains)
.set({ lastRunAt: candidate.lastRunAt, updatedAt: now })
.where(and(eq(dataDrains.id, candidate.id), eq(dataDrains.lastRunAt, now)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispatcher claim rollback uses reference-equal Date comparison

Low Severity

The claim step sets lastRunAt: now and the rollback's WHERE clause checks eq(dataDrains.lastRunAt, now) using the same Date object. While this works because drizzle serializes the Date to the same timestamp value, the rollback can silently succeed even when the runDrain service has already started and set its own lastRunAt, reverting a legitimately running drain's timestamp. This happens if queue.enqueue() throws after the job is actually accepted (e.g., network timeout after server-side commit). The service would later overwrite lastRunAt on completion, but there's a brief window where the drain could be re-dispatched by another cron tick.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit abdb0b6. Configure here.

and(eq(dataDrains.scheduleCadence, 'hourly'), lt(dataDrains.lastRunAt, hourlyCutoff)),
and(eq(dataDrains.scheduleCadence, 'daily'), lt(dataDrains.lastRunAt, dailyCutoff))
)
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dispatcher duePredicate object reused across SELECT and claim UPDATE

Low Severity

The duePredicate Drizzle expression object is built once and reused in both the initial SELECT and each per-candidate claim UPDATE. Drizzle expression objects carry bound parameter references; reusing the same object reference across multiple query builder calls relies on Drizzle treating the object as immutable. If Drizzle ever mutates the SQL node during query compilation (or caches compilation state), the second and subsequent uses of the same duePredicate reference in the claim loop could produce incorrect SQL. Building the predicate fresh per claim would be more defensive.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit abdb0b6. Configure here.

headers.Authorization = `Bearer ${input.credentials.bearerToken}`
}
return headers
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Webhook custom signatureHeader overwrites default without trace

Low Severity

In buildHeaders, when a custom signatureHeader is provided, the signature is placed under that key only — the default X-Sim-Signature header disappears entirely. Downstream receivers that hardcode checking X-Sim-Signature would silently see no signature when a custom header is configured. Meanwhile, the X-Sim-Signature-Version header is always sent regardless, creating a mismatch where the version header is present but the signature header it refers to doesn't exist under the documented default name. This is more of a design subtlety than a critical issue but could confuse integrators.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit abdb0b6. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant